spb/crypto-trade-bot
Public
Python 54.7%
TypeScript 42.5%
SQL 1.9%
1"use client";2import { useQuery } from "@tanstack/react-query";3import { Printer } from "lucide-react";4import { useParams } from "next/navigation";5import { BenchmarkTable, EquityVsBenchmarks, MetricsGrid, Provenance, StressTable, Underwater, WalkForward } from "@/components/run-views";6import { Badge, Button, Card, ErrorBox, KV, PageHeader, Spinner } from "@/components/ui";7import { api } from "@/lib/api";8import { fmtTs } from "@/lib/format";9import type { Run, StrategyMeta } from "@/lib/types";1011export default function ReportPage() {12 const { id } = useParams<{ id: string }>();13 const run = useQuery({ queryKey: ["run", id], queryFn: () => api<Run>(`/api/backtests/${id}`) });14 const strategies = useQuery({ queryKey: ["strategies"], queryFn: () => api<{ items: StrategyMeta[] }>("/api/strategies") });15 if (run.isLoading) return <Spinner />;16 if (run.error) return <ErrorBox error={run.error} />;17 const r = run.data!;18 const meta = strategies.data?.items.find((s) => s.name === r.strategy);19 const a = r.artifacts, m = r.metrics, p = r.provenance ?? {};20 if (r.status !== "done" || !a || !m) return <ErrorBox error={`Run is ${r.status}${r.error ? `: ${r.error}` : ""}`} />;21 const w = p.window as [number, number] | undefined;22 return (23 <article className="mx-auto max-w-5xl">24 <PageHeader title={`Research report — ${meta?.label ?? r.strategy}`} subtitle={<span>{r.symbol} · {r.interval_min}-minute bars · generated {new Date().toISOString().slice(0, 10)} · run {r.id}</span>} actions={<Button className="no-print" onClick={() => window.print()}><Printer className="h-3.5 w-3.5" /> Print / PDF</Button>} />25 <div className="mb-4 flex flex-wrap gap-2">{meta?.experimental && <Badge tone="warn">experimental — not validated for live trading</Badge>}<Badge tone="muted">{meta?.family}</Badge>{meta?.allows_short ? <Badge tone="muted">long/short</Badge> : <Badge tone="muted">long-only spot</Badge>}</div>26 <div className="flex flex-col gap-4">27 <Card title="1. Economic hypothesis"><p className="text-sm text-fg-muted">{meta?.hypothesis ?? "—"}</p>{meta?.references?.length ? <p className="mt-2 text-xs text-fg-dim">References: {meta.references.join("; ")}</p> : null}</Card>28 <Card title="2. Universe and data coverage">29 <KV items={[["Universe", `${r.symbol} (${meta?.universe_note ?? "Kraken spot"})`], ["Interval", `${r.interval_min} min`], ["Window", w ? `${fmtTs(w[0])} → ${fmtTs(w[1])}` : "—"], ["Closed bars", String(p.n_bars ?? "—")], ["Source", String(p.data_source ?? "—")], ["Dataset hash", String(p.dataset_hash ?? "—")], ["Holdout", `${Math.round(Number(p.holdout_frac ?? 0) * 100)}% reserved out-of-sample`]]} />30 </Card>31 <Card title="3. Parameters and cost assumptions"><KV items={[["Parameters", JSON.stringify(p.params ?? r.params)], ["Cost model", JSON.stringify(p.cost_model ?? r.cost_model)], ["Initial cash", String(p.initial_cash ?? "—")], ["Execution", String(p.evaluation_note ?? "—")]]} /></Card>32 <Card title="4. In-sample performance"><MetricsGrid m={m} /><div className="mt-4"><EquityVsBenchmarks a={a} label={r.strategy} /></div><div className="mt-4"><BenchmarkTable a={a} m={m} /></div><div className="mt-4"><Underwater a={a} /></div></Card>33 <Card title="5. Out-of-sample evidence">34 {a.holdout ? <><p className="mb-2 text-xs text-fg-muted">Holdout from {fmtTs(a.holdout.start_ts)} — never used for parameter selection.</p><MetricsGrid m={a.holdout.metrics} compact /></> : <p className="text-xs text-fg-muted">No holdout window was reserved for this run.</p>}35 <div className="mt-4"><WalkForward wf={a.walk_forward} /></div>36 </Card>37 <Card title="6. Uncertainty and multiple-testing control">38 <KV items={[["Sharpe 95% CI (block bootstrap)", m.bootstrap?.sharpe_ci ? `[${m.bootstrap.sharpe_ci[0]?.toFixed(2)}, ${m.bootstrap.sharpe_ci[1]?.toFixed(2)}] (${m.bootstrap.n_boot} resamples, block ${m.bootstrap.block})` : "—"], ["CAGR 95% CI", m.bootstrap?.cagr_ci ? `[${((m.bootstrap.cagr_ci[0] ?? 0) * 100).toFixed(1)}%, ${((m.bootstrap.cagr_ci[1] ?? 0) * 100).toFixed(1)}%]` : "—"], ["Deflated Sharpe", m.deflated_sharpe?.dsr != null ? `${(m.deflated_sharpe.dsr * 100).toFixed(1)}% probability over ${m.deflated_sharpe.n_trials} trials` : "—"], ["Shuffled trades", a.shuffled_trades?.n ? `median max DD ${a.shuffled_trades.max_dd_p50?.toFixed(2)}, worst ${a.shuffled_trades.max_dd_worst?.toFixed(2)} over ${a.shuffled_trades.n} permutations` : "n/a (< 5 round trips)"]]} />39 </Card>40 <Card title="7. Implementation costs and stress"><StressTable stress={a.stress} base={m} /></Card>41 <Card title="8. Failure modes and limitations">42 <ul className="list-disc space-y-1 pl-5 text-xs text-fg-muted">43 {(meta?.failure_modes ?? []).map((f) => <li key={f}>{f}</li>)}44 <li>History is accumulated from Kraken REST (≤720 bars per poll); coverage is limited to what has been collected since deployment unless a licensed dataset is loaded.</li>45 <li>Fills are simulated: no real queue priority, and partial fills are approximated by a participation cap.</li>46 <li>Results are not a claim of future profitability; experimental strategies must pass out-of-sample and paper-trading validation before live use.</li>47 </ul>48 </Card>49 <Card title="9. Provenance"><Provenance run={r} /></Card>50 </div>51 </article>52 );53}54